﻿
using Boilen.Primitives.Members;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Xml.Linq;


namespace Boilen.Primitives.Implementers {

    /// <inheritdoc/>
    /// <summary>
    /// Implements an existing dependency property.
    /// </summary>
    public sealed class ExistingDependencyProperty<T> : DependencyProperty<T> {

        private readonly DependencyProperty existingProperty_;
        private readonly string ownerTypeName_;
        private readonly string ownerDocTypeName_;
        private readonly XElement fieldDoc_;
        private readonly XElement accessorDoc_;
        private readonly MethodInfo attachedAccessor_;
        private readonly bool initializing_;

        private NewDependencyProperty<T> silverlightProperty_;


        /// <summary>
        /// Gets the existing dependency property the current property uses.
        /// </summary>
        public DependencyProperty ExistingProperty { get { return this.existingProperty_; } }

        /// <summary>
        /// Gets the type name of the <see cref="DependencyProperty.OwnerType"/> of the existing dependency property.
        /// </summary>
        public string OwnerTypeName { get { return this.ownerTypeName_; } }

        /// <summary>
        /// Gets a value indicating whether we are adding an owner for an attached dependency property.
        /// </summary>
        private bool IsExistingAttached { get { return this.attachedAccessor_ != null; } }


        /// <inheritdoc/>
        protected override IEnumerable<InitializationMember> Initializers {
            get {
                // Ignore validation for building registration arguments.
                var validate = this.Validate;
                this.Validate = Validate.None;
                var wpfArgs = this.BuildRegistrationArguments( false );
                this.Validate = validate;

                yield return new InitializationMember( this.FieldName, w => this.WriteExistingDependencyPropertyInitializer( w, wpfArgs ) ) {
                    MemberScope = this.Parent.TypeNamePrefix,
                    ReferencedExtensions = wpfArgs.UsedExtensions,
                    Condition = CompilationSymbol.NotSilverlight.Append( this.Condition )
                };
            }
        }


        /// <inheritdoc/>
        public ExistingDependencyProperty( PartialType parent, DependencyProperty existingProperty )
            : base( parent, GetDependencyPropertyName( existingProperty ), GetDependencyPropertyDescription( existingProperty ) ) {
            Ensure.ArgTypeMatches<T>( existingProperty.PropertyType );

            this.initializing_ = true;
            this.existingProperty_ = existingProperty;


            // Pull type and documentation info from existing property.
            Type ownerType = existingProperty.OwnerType;
            var typeRepository = this.Parent.TypeRepository;
            this.ownerTypeName_ = typeRepository.GetTypeName( ownerType );
            this.ownerDocTypeName_ = Doc.GetDocTypeName( typeRepository.GetTypeNamespace( ownerType ) + "." + typeRepository.GetTypeName( ownerType ) );
            this.SetReadOnly( existingProperty.ReadOnly );

            this.attachedAccessor_ = ownerType.GetMethod( "Get" + existingProperty.Name );
            if( GlobalSettings.ExternalDocumentationPrefix == null ) {
                var field = GetDependencyPropertyField( existingProperty );
                this.fieldDoc_ = XmlDocumentation.GetDocMember( field );

                if( this.attachedAccessor_ == null ) {
                    var accessor = ownerType.GetProperty( existingProperty.Name );
                    this.accessorDoc_ = XmlDocumentation.GetDocMember( accessor );
                }
                else {
                    this.accessorDoc_ = null;
                }
            }
            else {
                this.fieldDoc_ = null;
                this.accessorDoc_ = null;
            }


            // Pull metadata info from existing property.
            var metadata = this.existingProperty_.DefaultMetadata;
            T defaultValue = (T)metadata.DefaultValue;

            var existingOptions = FrameworkPropertyMetadataOptions.None;
            var frameworkMetadata = metadata as FrameworkPropertyMetadata;
            if( frameworkMetadata != null ) {
                var optionValues = (FrameworkPropertyMetadataOptions[])Enum.GetValues( typeof( FrameworkPropertyMetadataOptions ) );
                var optionAccessors =
                    from value in optionValues
                    let property = typeof( FrameworkPropertyMetadata ).GetProperty( value.ToString( ), typeof( bool ) )
                    where property != null
                    select new { Value = value, Property = property };

                foreach( var oa in optionAccessors ) {
                    bool isSet = (bool)oa.Property.GetValue( frameworkMetadata, null );
                    if( isSet )
                        existingOptions |= oa.Value;
                }
            }

            this.SetDefaultValue( defaultValue );
            this.Options = existingOptions;

            if( metadata.PropertyChangedCallback != null ) {
                this.Changed = Changed.Static;
            }

            if( metadata.CoerceValueCallback != null ) {
                string handler = GetExternalHandler( metadata.CoerceValueCallback.Method );
                if( handler != null )
                    SetCoerce( handler );
                else
                    SetCoerce( Coerce.Custom );
            }

            if( this.existingProperty_.ValidateValueCallback != null ) {
                if( typeof( T ).IsEnum )
                    SetValidate( Validate.Enum );
                else {
                    string handler = GetExternalHandler( this.existingProperty_.ValidateValueCallback.Method );
                    if( handler != null )
                        SetValidate( handler );
                    else
                        SetValidate( Validate.Custom );
                }
            }

            this.initializing_ = false;
        }

        private string GetExternalHandler( MethodInfo method ) {
            if( this.Parent.Type.IsAssignableFrom( method.DeclaringType ) )
                return null;

            string typeName = this.Parent.TypeRepository.GetTypeName( method.DeclaringType );
            string handlerName = typeName + "." + method.Name;

            if( method.IsGenericMethod ) {
                var typeArguments = method.GetGenericArguments( ).Select( this.Parent.TypeRepository.GetTypeName );
                handlerName += "<" + string.Join( ",", typeArguments ) + ">";
            }

            return handlerName;
        }


        /// <inheritdoc/>
        public override DependencyProperty<T> SetValidate( Validate validate ) {
            if( this.initializing_ )
                return base.SetValidate( validate );
            throw new InvalidOperationException( "Cannot change validation of an existing dependency property." );
        }

        /// <inheritdoc/>
        public override void Prepare( ) {
            base.Prepare( );

            if( this.Attached || this.IsReadOnly )
                throw new InvalidOperationException( "Existing Dependency Property cannot be attached, read-only, or use custom validation." );

            if( !this.IsExistingAttached ) {
                // Create new dependency property for silverlight.
                this.silverlightProperty_ = new NewDependencyProperty<T>( this.Parent, this.Name, this.Description ) {
                    Changed = this.Changed,
                    ChangedHandler = this.ChangedHandler,
                    Coerce = this.Coerce,
                    CoerceHandler = this.CoerceHandler,
                    Validate = this.Validate,
                    ValidateHandler = this.ValidateHandler,
                    Options = this.Options,
                    IsParentFreezable = this.IsParentFreezable,
                    FromExistingProperty = true
                };

                foreach( var pair in this.DefaultValues )
                    this.silverlightProperty_.AddDefaultExpression( pair.Key, pair.Value );

                this.silverlightProperty_.Prepare( );
            }
        }


        /// <inheritdoc/>
        protected override IEnumerable<Member> GetMembers( ) {
            var baseMembers = base.GetMembers( );
            var silverlightPropertyMembers =
                this.IsExistingAttached
                    ? new Member[] {
                        new InitializationMember( this.FieldName, this.OwnerTypeName + "." + this.FieldName ) {
                            MemberScope = this.Parent.TypeNamePrefix,
                            Condition = CompilationSymbol.Silverlight.Append( this.Condition )
                        } }
                    : this.silverlightProperty_.Members
                          .Where( m => m.Condition.Equals( CompilationSymbol.Silverlight ) )
                          .Select( m => { m.Condition = m.Condition.Append( this.Condition ); return m; } );

            return baseMembers.Concat( silverlightPropertyMembers );
        }

        /// <inheritdoc/>
        protected override Doc CreateFieldDoc( ) {
            return this.fieldDoc_ == null
                 ? base.CreateFieldDoc( )
                 : this.CreateDependencyPropertyDoc( )
                       .AddDocElements( this.fieldDoc_.Elements( ) );
        }

        /// <inheritdoc/>
        protected override Doc CreateAccessorDoc( ) {
            Doc doc = this.CreateDependencyPropertyDoc( );
            if( GlobalSettings.ExternalDocumentationPrefix != null )
                doc.UseIncludeTag( );
            else if( this.IsExistingAttached )
                doc.InheritFrom = "P:" + this.ownerDocTypeName_ + "." + this.AccessorName;
            else
                doc.AddDocElements( this.accessorDoc_.Elements( ) );

            return doc;
        }

        private Doc CreateDependencyPropertyDoc( ) {
            return this.CreateDoc( )
                .AddReplacement( '"' + this.ExistingProperty.OwnerType.FullName + "." + this.AccessorName + '"', "\"P:{0}.{1}\"", Doc.GetDocTypeName( this.Parent.TypeFullName ), this.AccessorName )
                .AddReplacement( '"' + this.ExistingProperty.OwnerType.FullName, "\"{0}", Doc.GetDocTypeName( this.Parent.TypeName ) );
        }

        private void WriteExistingDependencyPropertyInitializer( ICodeWriter writer, DependencyPropertyMetadata args ) {
            Ensure.ArgSatisfies( !args.Silverlight, "args", "AddOwner only works for non-Silverlight builds." );

            // Check if any metadata changes were made, requiring metadata registration.
            bool requiresMetadata = (args.MetadataKind != DependencyPropertyMetadataKind.Base) || args.ChangedHandler != null;
            if( !requiresMetadata ) {
                var metadata = this.existingProperty_.DefaultMetadata;
                T defaultValue = (T)metadata.DefaultValue;
                string defaultValueString = this.Parent.TypeRepository.GetValueString( defaultValue, false );

                string assignedDefaultValue;
                if( !this.DefaultValues.TryGetValue( CompilationSymbol.None, out assignedDefaultValue ) )
                    assignedDefaultValue = this.DefaultValues[CompilationSymbol.NotSilverlight];

                requiresMetadata = (defaultValueString != assignedDefaultValue);
            }

            writer.Write( "{0}.{1}.AddOwner", this.OwnerTypeName, this.FieldName );
            using( Enclose.Parenthesis( writer ) ) {
                string parentTypeName = string.Format( "typeof({0})", this.Parent.TypeName );
                if( !requiresMetadata )
                    writer.Write( parentTypeName );
                else
                    using( Enclose.NewLine( writer ) )
                    using( Enclose.Indent( writer ) ) {
                        writer.WriteLine( parentTypeName + "," );
                        args.Write( writer );
                    }
            }
        }


        private static FieldInfo GetDependencyPropertyField( DependencyProperty existingProperty ) {
            Ensure.NotNull( existingProperty );
            return existingProperty.OwnerType.GetField( existingProperty.Name + MemberKind );
        }

        private static string GetDependencyPropertyName( DependencyProperty existingProperty ) {
            Ensure.NotNull( existingProperty );
            return existingProperty.Name;
        }

        private static string GetDependencyPropertyDescription( DependencyProperty existingProperty ) {
            if( GlobalSettings.ExternalDocumentationPrefix != null )
                return null;

            var field = GetDependencyPropertyField( existingProperty );
            var summary = XmlDocumentation.GetSummary( field );
            return summary;
        }

    }

}
